import { createFlowchartSolver } from "./flowchart.js";
import { getTagMap, loadFlowchart } from "./flowchart.js";

const FPS = 25;
import { SOLVER_FPS } from "./flowchart.js";
import solver from "./flowcharts/solver.js";


const PLAYER_SPEED_CAP = 400;
const PLAYER_SPEED_PER_FRAME_CAP = PLAYER_SPEED_CAP / FPS;
const PLAYER_ACCELERATION = 200;
const PLAYER_STOP_ACCELERATION = PLAYER_SPEED_CAP;
const PLAYER_ACCELERATION_PER_FRAME = PLAYER_ACCELERATION / FPS;
const PLAYER_STOP_ACCELERATION_PER_FRAME = PLAYER_STOP_ACCELERATION / FPS;

const PLAYER_WIDTH = 50;
const PLAYER_HEIGHT = 50;
const EYE_SIZE = 3;

const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 600;

window.saveCode = (code) => {
    console.log(code);
    window.localStorage.setItem("code", code);
}

window.addEventListener("beforeunload", () => {
    window.saveCode(window.getCode());
});

/**
 * @typedef { [x: number, y: number] Vec }
 */

/**
 * 
 * @typedef { {start: Vec, destination: Vec, timeout: number, xFinished: boolean, yFinished: boolean, resolve: (success: boolean) => void} } Destination
 */

function createInstance() {
    /**
     * @type { HTMLCanvasElement }
     */
    const canvas = document.getElementById("game");
    const instance = {
        /**
         * @type { import("./flowchart.js").FlowchartSolver }
         */
        solver,
        canvas,
        canvasFocused: true,
        context: canvas.getContext("2d"),
        /**
         * @type { Vec }
         */
        playerPosition: [400, 300],
        /**
         * @type { Vec }
         */
        playerAcceleration: [0, 0],
        /**
         * @type { Vec }
         */
        playerSpeed: [0, 0],
        /**
         * @type { Destination[] }
         */
        destinations: [],
        /**
         * @type { Vec }
         */
        mousePosition: [0, 0],
        /**
         * @type { Set<string> }
         */
        keydown: new Set(),
        /**
         * @type { Set<string> }
         */
        keyClicked: new Set(),
        /**
         * @type { Map<String, (() => void)[]> }
         */
        keyClickCallbacks: new Map(),
        /**
         * @type { Map<String, (() => void)[]> }
         */
        keyPressCallbacks: new Map(),
        /**
         * @type { Vec }
         */
        eyeCenter: [0, 0],
        abilities: {

            debug: {
                log: (...text) => instance.logs.push(text.join(" "))
            },
            player: {
                /**
                 * @type { (x: number, y: number, timeout?: number) => void }
                 */
                moveTo: undefined,
                move: undefined,
                get x() {
                    return instance.playerPosition[0];
                },
                get y() {
                    return instance.playerPosition[1];
                },
                get speedX() {
                    return instance.playerSpeed[0];
                },
                get speedY() {
                    return instance.playerSpeed[1];
                },
                get accelerationX() {
                    return instance.playerAcceleration[0];
                },
                get accelerationY() {
                    return instance.playerAcceleration[1];
                }
            }
        },
        /**
         * @type { [number, number] }
         */
        endPosition: undefined,
        /**
         * @type { number }
         */
        timeout: undefined,
        /**
         * @type { [Vec, Vec][] }
         */
        obstacles: [],
        active: true,
        /**
         * @type { string[] }
         */
        logs: []
    };
    instance.abilities.player.moveTo = moveToAbility.bind(undefined, instance);
    instance.abilities.player.move = moveAbility.bind(undefined, instance);
    return instance;
}


/**
 * @typedef { ReturnType<createInstance> } Instance
 */

/**
 * @param { Instance } instance 
 */
function playerMove(instance) {
    let auto = false;
    /**
     * @type { Destination }
     */
    let destination;
    if (instance.destinations[0]) {
        auto = true;
        destination = instance.destinations[0];
    }

    if (auto) {
        if (!destination.xFinished) {
            instance.playerAcceleration[0] = PLAYER_ACCELERATION_PER_FRAME * (destination.destination[0] - instance.playerPosition[0] > 0 ? 1 : -1);
        }
        if (!destination.yFinished) {
            instance.playerAcceleration[1] = PLAYER_ACCELERATION_PER_FRAME * -(destination.destination[1] - instance.playerPosition[1] > 1 ? 1 : -1);
        }
    }

    let xMoving = true;
    let yMoving = true;
    if (instance.playerAcceleration[0] == 0) {
        instance.playerAcceleration[0] = (instance.playerSpeed[0] > 0 ? -1 : 1) * PLAYER_STOP_ACCELERATION_PER_FRAME;
        xMoving = false;
    }

    if (instance.playerAcceleration[1] == 0) {
        instance.playerAcceleration[1] = (instance.playerSpeed[1] > 0 ? -1 : 1) * PLAYER_STOP_ACCELERATION_PER_FRAME;
        yMoving = false;
    }

    if (!xMoving && instance.playerSpeed[0] * (instance.playerSpeed[0] + instance.playerAcceleration[0]) <= 0) {
        instance.playerSpeed[0] = 0;
    } else {
        instance.playerSpeed[0] = Math.max(-PLAYER_SPEED_PER_FRAME_CAP, Math.min(PLAYER_SPEED_PER_FRAME_CAP, instance.playerSpeed[0] + instance.playerAcceleration[0]));
    }

    if (!yMoving && instance.playerSpeed[1] * (instance.playerSpeed[1] + instance.playerAcceleration[1]) <= 0) {
        instance.playerSpeed[1] = 0;
    } else {
        instance.playerSpeed[1] = Math.max(-PLAYER_SPEED_PER_FRAME_CAP, Math.min(PLAYER_SPEED_PER_FRAME_CAP, instance.playerSpeed[1] + instance.playerAcceleration[1]));
    }

    if (auto && !destination.xFinished && (instance.playerPosition[0] - destination.destination[0]) * (instance.playerPosition[0] + instance.playerSpeed[0] - destination.destination[0]) <= 0) {
        destination.xFinished = true;
        instance.playerSpeed[0] = 0;
        instance.playerAcceleration[0] = 0;
        console.log("x finished:", instance.playerPosition[0]);
    }

    if (auto && !destination.yFinished && (instance.playerPosition[1] - destination.destination[1]) * (instance.playerPosition[1] - instance.playerSpeed[1] - destination.destination[1]) <= 0) {
        destination.yFinished = true;
        instance.playerSpeed[1] = 0;
        instance.playerAcceleration[1] = 0;
        console.log("y finished:", instance.playerPosition[1]);
    }

    if (instance.playerSpeed[0] > 0.1) {
        instance.eyeCenter[0] = PLAYER_WIDTH / 6;
    } else if (instance.playerSpeed[0] > -0.1) {
        instance.eyeCenter[0] = 0;
    } else {
        instance.eyeCenter[0] = -PLAYER_WIDTH / 6;
    }

    if (instance.playerSpeed[1] > 0.1) {
        instance.eyeCenter[1] = -PLAYER_WIDTH / 6;
    } else if (instance.playerSpeed[1] > -0.1) {
        instance.eyeCenter[1] = 0;
    } else {
        instance.eyeCenter[1] = PLAYER_WIDTH / 6;
    }
    applyMovement(instance, ...instance.playerSpeed);
    if (auto && (Date.now() >= destination.timeout || (destination.xFinished && destination.yFinished))) {
        instance.destinations = instance.destinations.slice(1);
        destination.resolve(Date.now() < destination.timeout);
    }
}

/**
 * 
 * @param { number } min 
 * @param { number } max 
 * @param { number } x 
 */
function within(min, max, x) {
    if (x < min) return min;
    if (x > max) return max;
    return x;
}

/**
 * 确定两条线段的交点
 * @param {[[number, number], [number, number]]} line0 - 第一条线段的两个端点 [[x0, y0], [x1, y1]]
 * @param {[[number, number], [number, number]]} line1 - 第二条线段的两个端点 [[x2, y2], [x3, y3]]
 * @returns {[number, number]?} - 线段的交点 [x, y]
 */
function collide(line0, line1) {
    const [point0, point1] = line0;
    const [x0, y0] = point0;
    const [x1, y1] = point1;

    const [point2, point3] = line1;
    const [x2, y2] = point2;
    const [x3, y3] = point3;

    // 检查是否垂直
    if (x0 === x1) {
        // line0 垂直
        if (x2 !== x3) {
            // line1 不垂直
            const t = (x0 - x2) / (x3 - x2);
            if (t >= 0 && t <= 1) {
                const y = y2 + t * (y3 - y2);
                if (y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
                    return [x0, y];
                }
            }
        }
    } else if (x2 === x3) {
        // line1 垂直
        const t = (x2 - x0) / (x1 - x0);
        if (t >= 0 && t <= 1) {
            const y = y0 + t * (y1 - y0);
            if (y >= Math.min(y2, y3) && y <= Math.max(y2, y3)) {
                return [x2, y];
            }
        }
    } else {
        // 两者都不垂直
        const slope0 = (y1 - y0) / (x1 - x0);
        const slope1 = (y3 - y2) / (x3 - x2);
        const intercept0 = y0 - slope0 * x0;
        const intercept1 = y2 - slope1 * x2;

        const x = (intercept1 - intercept0) / (slope0 - slope1);
        const y = slope0 * x + intercept0;

        // 检查交点是否在两条线段上
        if (x >= Math.min(x0, x1) && x <= Math.max(x0, x1) &&
            x >= Math.min(x2, x3) && x <= Math.max(x2, x3)) {
            return [x, y];
        }
    }

    return undefined;
}

function segmentsIntr([a, b], [c, d]) {
    a = {
        x: a[0],
        y: a[1]
    };

    b = {
        x: b[0],
        y: b[1]
    };

    c = {
        x: c[0],
        y: c[1]
    };

    d = {
        x: d[0],
        y: d[1]
    };
    /** 1 解线性方程组, 求线段交点. **/
    // 如果分母为0 则平行或共线, 不相交  
    let denominator = (b.y - a.y) * (d.x - c.x) - (a.x - b.x) * (c.y - d.y);
    if (denominator == 0) {
        return false;
    }

    // 线段所在直线的交点坐标 (x , y)      
    let x = ((b.x - a.x) * (d.x - c.x) * (c.y - a.y)
        + (b.y - a.y) * (d.x - c.x) * a.x
        - (d.y - c.y) * (b.x - a.x) * c.x) / denominator;
    let y = -((b.y - a.y) * (d.y - c.y) * (c.x - a.x)
        + (b.x - a.x) * (d.y - c.y) * a.y
        - (d.x - c.x) * (b.y - a.y) * c.y) / denominator;


    /** 2 判断交点是否在两条线段上 **/
    if (
        // 交点在线段1上  
        (x - a.x) * (x - b.x) <= 0 && (y - a.y) * (y - b.y) <= 0
        // 且交点也在线段2上  
        && (x - c.x) * (x - d.x) <= 0 && (y - c.y) * (y - d.y) <= 0
    ) {

        // 返回交点p  
        return {
            x: x,
            y: y
        }
    }
    //否则不相交  
    return false

}

/**
 * 
 * @param { Instance } instance 
 * @param { number } deltaX 
 * @param { number } deltaY 
 */
function applyMovement(instance, deltaX, deltaY) {
    const currentX = instance.playerPosition[0];
    const currentY = instance.playerPosition[1];

    let newX = instance.playerPosition[0] + deltaX;
    let newY = instance.playerPosition[1] - deltaY;

    newX = within(PLAYER_WIDTH / 2, CANVAS_WIDTH - PLAYER_WIDTH / 2, newX);
    newY = within(PLAYER_HEIGHT / 2, CANVAS_HEIGHT - PLAYER_HEIGHT / 2, newY);

    for (const obstacle of instance.obstacles) {
        const [[startX, startY], [endX, endY]] = obstacle;
        /** @type {  [number, number, number, number, string][] } */
        const sides = [
            [startX, startY - PLAYER_HEIGHT / 2, startX, endY + PLAYER_HEIGHT / 2, "left"],
            [startX - PLAYER_WIDTH / 2, startY, endX + PLAYER_WIDTH / 2, startY, "up"],
            [endX, startY - PLAYER_HEIGHT / 2, endX, endY + PLAYER_HEIGHT / 2, "right"],
            [startX - PLAYER_WIDTH / 2, endY, endX + PLAYER_WIDTH / 2, endY, "down"]
        ];
        for (let [borderStartX, borderStartY, borderEndX, borderEndY, direction] of sides) {
            if (direction === "left") borderStartX = borderEndX = borderStartX - PLAYER_WIDTH / 2;
            if (direction === "right") borderStartX = borderEndX = borderStartX + PLAYER_WIDTH / 2;
            if (direction === "down") borderStartY = borderEndY = borderStartY + PLAYER_HEIGHT / 2;
            if (direction === "up") borderStartY = borderEndY = borderStartY - PLAYER_HEIGHT / 2;

            const collidePoint = segmentsIntr([[currentX, currentY], [newX, newY]], [[borderStartX, borderStartY], [borderEndX, borderEndY]]);

            if (!collidePoint) continue;
            if (!collidePoint.x || !collidePoint.y) continue;

            if (direction === "left" && currentX > borderStartX) continue;
            if (direction === "right" && currentX < borderEndX) continue;
            if (direction === "up" && currentY > borderEndY) continue;
            if (direction === "down" && currentY < borderStartY) continue;

            //instance.abilities.debug.log(1);

            if (direction === "left" && (newX <= borderStartX)) continue;
            if (direction === "right" && (newX >= borderEndX)) continue;
            if (direction === "up" && (newY <= borderEndY)) continue;
            if (direction === "down" && (newY >= borderStartY)) continue;

            //instance.abilities.debug.log(currentX, currentY, newX, newY, "border:", direction, borderStartX, borderStartY, borderEndX, borderEndY, "collide", JSON.stringify(segmentsIntr([[currentX, currentY], [newX, newY]], [[borderStartX, borderStartY], [borderEndX, borderEndY]])));

            if (direction === "left") {
                newX = borderEndX;
            }
            if (direction === "right") {
                newX = borderStartX;
            }
            if (direction === "down") {
                newY = borderStartY;
            }
            if (direction === "up") {
                newY = borderEndY;
            }
            //instance.abilities.debug.log(direction, newX, newY);
        }
    }

    instance.playerPosition[0] = newX;
    instance.playerPosition[1] = newY;
}

/**
 * 
 * @param { Instance } instance 
 * @param { Vec } nextPos 
 */
function checkCollide(instance, nextPos) {
    const curPos = instance.playerPosition;
}

/**
 * 
 * @param { Vec } a 
 * @param { Vec } b 
 * @param { Vec } c 
 * @param { Vec } d 
 * @returns { Vec? }
 */
function lineCollide(a, b, c, d) {
    const line1Start = [Math.min(a[0], b[0]), Math.min(a[1], b[1])];
    const line1End = [Math.max(a[0], b[0]), Math.max(a[1], b[1])];
    const line2Start = [Math.min(c[0], d[0]), Math.min(c[1], d[1])];
    const line2End = [Math.max(c[0], d[0]), Math.max(c[1], d[1])];

    if (line1End[0] < line2Start[0]) return undefined;
    if (line1Start[0] > line2End[0]) return undefined;

    if (line1End[1] < line2Start[1]) return undefined;
    if (line1Start[1] > line2End[1]) return undefined;

    if ((line1Start[0] - line2Start[0]) * (line2End[0] - line2Start[0]) + (line1Start[1] - line2Start[1]) * (line2End[1] - line2Start[1]) *
        (line1End[0] - line2Start[0]) * (line2End[0] - line2Start[0]) + (line1End[1] - line2Start[1]) * (line2End[1] - line2Start[1]) >= 0) return undefined;

    if ((line2Start[0] - line1Start[0]) * (line1End[0] - line1Start[0]) + (line2Start[1] - line1Start[1]) * (line1End[1] - line1Start[1]) *
        (line2End[0] - line1Start[0]) * (line1End[0] - line1Start[0]) + (line2End[1] - line1Start[1]) * (line1End[1] - line1Start[1]) >= 0) return undefined;

    // 提取线段端点的坐标
    const [x1, y1] = line1Start;
    const [x2, y2] = line1End;
    const [x3, y3] = line2Start;
    const [x4, y4] = line2End;

    // 计算分母
    const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);

    // 检查是否平行或共线
    if (denominator === 0) {
        // 这里可以添加额外的逻辑来处理共线但不重叠的情况，但通常我们只需要知道是否平行
        // 如果需要处理共线情况，可以参考之前的代码
        return undefined; // 线段平行或共线（但不处理共线且重叠的情况）
    }

    // 计算参数t和u
    const t = ((x3 - x1) * (y4 - y3) - (y3 - y1) * (x4 - x3)) / denominator;
    const u = ((x1 - x3) * (y2 - y1) - (y1 - y3) * (x2 - x1)) / denominator;

    // 检查交点是否在线段上
    if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
        // 计算交点坐标
        const intersectionX = x1 + t * (x2 - x1);
        const intersectionY = y1 + t * (y2 - y1);
        return [intersectionX, intersectionY]; // 返回交点坐标数组
    }

    // 如果没有交点
    return undefined;
}

/**
 * 
 * @param { Instance } instance 
 */
function listenMousePosition(instance) {
    instance.canvas.addEventListener("mousemove", (ev) => {
        const canvasRect = instance.canvas.getBoundingClientRect();
        instance.mousePosition[0] = ev.clientX - canvasRect.left;
        instance.mousePosition[1] = ev.clientY - canvasRect.top;
    });
}

/**
 * 
 * @param { Instance } instance 
 */
function triggerKeyPressCallbacks(instance) {
    instance.keydown.forEach((key) => {
        const callbacks = instance.keyPressCallbacks.get(key);
        if (!callbacks) return;
        callbacks.forEach(callback => callback());
    });
}

/**
 * 
 * @param { Instance } instance 
 */
function triggerKeyClickCallbacks(instance) {
    instance.keyClicked.forEach((key) => {
        const callbacks = instance.keyClickCallbacks.get(key);
        if (!callbacks) return;
        callbacks.forEach(callback => callback());
    });
    instance.keyClicked.clear();
}

/**
 * 
 * @param { Instance } instance 
 * @param { string } key 
 * @param { () => void } callback 
 */
function onKeyClick(instance, key, callback) {
    instance.keyClickCallbacks.set(key, instance.keyClickCallbacks.has(key) ? [...instance.keyClickCallbacks.get(key), callback] : [callback]);
}

/**
 * 
 * @param { Instance } instance 
 * @param { string } key 
 * @param { () => void } callback 
 */
function onKeyPress(instance, key, callback) {
    instance.keyPressCallbacks.set(key, instance.keyPressCallbacks.has(key) ? [...instance.keyPressCallbacks.get(key), callback] : [callback]);
}

/**
 * @param { Instance } instance 
 * @param { string } code 
 */
async function evalWithAbilities(instance, code) {
    try {
        await (new Function(Object.keys(instance.abilities).join(","), `return (async function() {${code}})()`)).apply(undefined, Object.values(instance.abilities));
    }
    catch (e) {
        instance.logs.push(e);
    }
}

/**
 * 
 * @param { Instance } instance 
 */
async function load(instance) {
    instance.canvas.addEventListener("keydown", (ev) => {
        if (!instance.canvasFocused) return;
        if (!instance.keydown.has(ev.key)) {
            instance.keyClicked.add(ev.key);
        }
        instance.keydown.add(ev.key);
    });
    instance.canvas.addEventListener("keyup", (ev) => {
        instance.keydown.delete(ev.key);
    });

    instance.canvas.addEventListener("focus", () => instance.canvasFocused = true);
    instance.canvas.addEventListener("blur", () => instance.canvasFocused = false);

    onKeyPress(instance, "a", () => instance.playerAcceleration[0] -= PLAYER_ACCELERATION_PER_FRAME);
    onKeyPress(instance, "d", () => instance.playerAcceleration[0] += PLAYER_ACCELERATION_PER_FRAME);
    onKeyPress(instance, "s", () => instance.playerAcceleration[1] -= PLAYER_ACCELERATION_PER_FRAME);
    onKeyPress(instance, "w", () => instance.playerAcceleration[1] += PLAYER_ACCELERATION_PER_FRAME);

    /**
     * test auto move
     */
    onKeyClick(instance, "c", () => {
        instance.abilities.player.moveTo(300, 300);
    });
    onKeyPress(instance, "p", () => console.log("test press"));
    listenMousePosition(instance);
    listenButton(instance);
}

/**
 * 
 * @param { Instance } instance 
 */
function listenButton(instance) {
    /**
     * @type { HTMLButtonElement }
     */
    const runButton = document.getElementById("run_btn");
    runButton.addEventListener("click", async () => {
        try {
            await instance.solver.run(instance.abilities, instance.timeout);
        } catch (e) {
            /**
             * @type { string }
             */
            const id = e.id;
            if (id === "incomplete_chart") {
                window.alert("流程图不完整！");
            } if (id === "timeout") {
                window.alert("运行超时！");
            }
            else {
                throw e.error;
            }
        }

    });

    const resetButton = document.getElementById("reset_btn");
    resetButton.addEventListener("click", () => {
        window.location.reload();
    });
}

/**
 * 
 * @param { Instance } instance 
 */
function resetStates(instance) {
    instance.playerAcceleration = [0, 0];
}

/**
 * 
 * @param { Instance } instance 
 */
function drawDebug(instance) {
    instance.context.textAlign = "start";
    instance.context.fillStyle = "black";
    instance.context.font = "Consolas 30px";

    instance.context.fillText(`玩家位置: ${instance.playerPosition[0]} ${instance.playerPosition[1]}`, 10, 10);
    instance.context.fillText(`玩家速度: ${instance.playerSpeed[0]} ${instance.playerSpeed[1]}`, 10, 25);
    instance.context.fillText(`鼠标位置: ${instance.mousePosition[0]} ${instance.mousePosition[1]}`, 10, 40);
    instance.context.fillText(`时间限制: ${instance.timeout / 1000}秒`, 10, 55);

    instance.logs.slice().reverse().forEach((log, index) => {
        instance.context.textAlign = "end";
        instance.context.fillText(log, CANVAS_WIDTH - 10, CANVAS_HEIGHT - (index + 1) * 10);
    });
}

const ENDPOINT_BOTTOM_RADIUS_X = 16;
const ENDPOINT_BOTTOM_RADIUS_Y = 8;
const ENDPOINT_FLAG_STICK_HEIGHT = 60;
const ENDPOINT_FLAG_HEIGHT = 25;
const ENDPOINT_FLAG_WIDTH = 60;

/**
 * 
 * @param { Instance } instance 
 */
function drawEnd(instance) {
    instance.context.fillStyle = "red";
    instance.context.strokeStyle = "black";
    instance.context.beginPath();
    instance.context.ellipse(instance.endPosition[0], instance.endPosition[1], ENDPOINT_BOTTOM_RADIUS_X, ENDPOINT_BOTTOM_RADIUS_Y, Math.PI * 2, 0, Math.PI * 2);
    instance.context.stroke();
    instance.context.fill();

    instance.context.beginPath();
    instance.context.moveTo(...instance.endPosition);
    instance.context.lineTo(instance.endPosition[0], instance.endPosition[1] - ENDPOINT_FLAG_STICK_HEIGHT);
    instance.context.lineTo(instance.endPosition[0] + ENDPOINT_FLAG_WIDTH / 2, instance.endPosition[1] - ENDPOINT_FLAG_STICK_HEIGHT + ENDPOINT_FLAG_HEIGHT / 2);
    instance.context.lineTo(instance.endPosition[0], instance.endPosition[1] - ENDPOINT_FLAG_STICK_HEIGHT + ENDPOINT_FLAG_HEIGHT);
    instance.context.stroke();
    instance.context.fill();
}

/**
 * 
 * @param { Instance } instance 
 */
function draw(instance) {
    instance.context.fillStyle = "white";
    instance.context.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    drawObstacles(instance);
    drawEnd(instance);
    drawPlayer(instance);
    drawDebug(instance);
}

/**
 * 
 * @param { Instance } instance 
 */
function drawObstacles(instance) {
    instance.obstacles.forEach(obstacle => {
        const [[startX, startY], [endX, endY]] = obstacle;
        const width = endX - startX;
        const height = endY - startY;

        instance.context.strokeStyle = "black";
        instance.context.strokeRect(startX, startY, width, height);
    });
}

/**
 * 
 * @param { Instance } instance 
 */
function drawPlayer(instance) {
    instance.context.fillStyle = "black";
    instance.context.fillRect(instance.playerPosition[0] - PLAYER_WIDTH / 2, instance.playerPosition[1] - PLAYER_HEIGHT / 2, PLAYER_WIDTH, PLAYER_HEIGHT);
    instance.context.fillStyle = "white";
    const eyeCenter = [
        instance.playerPosition[0] + instance.eyeCenter[0],
        instance.playerPosition[1] + instance.eyeCenter[1]
    ];
    instance.context.fillRect(eyeCenter[0] - PLAYER_WIDTH / 6 - EYE_SIZE / 2, eyeCenter[1] - EYE_SIZE / 2, EYE_SIZE, EYE_SIZE);
    instance.context.fillRect(eyeCenter[0] + PLAYER_WIDTH / 6 - EYE_SIZE / 2, eyeCenter[1] - EYE_SIZE / 2, EYE_SIZE, EYE_SIZE);
}

/**
 * @param { Vec } base 
 * @param { Vec } pos 
 */
function getRenderPosition(base, pos) {
    return [pos[0] - base[0], pos[1] - base[1]];
}

/**
 * @param { Instance } instance 
 * @param { number } x 
 * @param { number } y 
 * @param { number? } timeout
 * @returns { Promise<void> }
 */
function moveToAbility(instance, x, y, timeout) {
    console.log(`simple auto move to ${x} ${y}`);

    if (!timeout) timeout = 1000 * 30;
    return new Promise(resolve => {
        instance.destinations.push({
            start: [instance.playerPosition.slice()],
            destination: [x, y],
            timeout: Date.now() + timeout,
            xFinished: false,
            yFinished: false,
            resolve
        });
    });
}

/**
 * @param { Instance } instance 
 * @param { number } x 
 * @param { number } y 
 * @param { number? } timeout
 * @returns { Promise<void> }
 */
async function moveAbility(instance, x, y, timeout) {
    await moveToAbility(instance, instance.playerPosition[0] + x, instance.playerPosition[1] + y, timeout);
}

const ENDPOINT_RECT_WIDTH = 100;
const ENDPOINT_RECT_HEIGHT = 100;

/**
 * @param { Instance } instance 
 */
function checkWin(instance) {
    if (instance.playerPosition[0] >= instance.endPosition[0] - ENDPOINT_RECT_WIDTH / 2 && instance.playerPosition[0] <= instance.endPosition[0] + ENDPOINT_RECT_WIDTH / 2 && instance.playerPosition[1] >= instance.endPosition[1] - ENDPOINT_RECT_HEIGHT / 2 && instance.playerPosition[1] <= instance.endPosition[1] + ENDPOINT_RECT_HEIGHT / 2) {
        instance.active = false;
        instance.solver.active = false;
        window.alert("关卡成功！");
    }
}

/**
 * 
 * @param { Instance } instance 
 */
async function loop(instance) {
    if (!instance.active) return;
    resetStates(instance);
    triggerKeyPressCallbacks(instance);
    triggerKeyClickCallbacks(instance);
    playerMove(instance);
    checkWin(instance);

    draw(instance);
}

/**
 * 
 * @param { Instance } instance 
 * @param { string } levelURL 
 */
async function loadLevel(instance, levelURL) {
    const levelData = await (await fetch(levelURL)).json();
    instance.playerPosition = levelData.spawnPosition.slice();
    instance.endPosition = levelData.endPosition.slice();
    instance.timeout = levelData.timeout;
    instance.obstacles = levelData.obstacles;

    const solverChart = await loadFlowchart(levelData.chart);
    const solver = window.solver = createFlowchartSolver(document.getElementById("solver"), solverChart);
    //solver.tick();

    instance.solver = solver;
}

window.addEventListener("load", async () => {
    const instance = createInstance();
    await loadLevel(instance, `/levels/${["simple_movement", "pillars", "lab"][Math.floor(Math.random() * 3)]}.json`);
    instance.solver.startListenMouse();
    setInterval(instance.solver.tick.bind(instance.solver), 1000 / SOLVER_FPS);
    await load(instance);
    setInterval(loop, 1000 / FPS, instance);
});